// (c) Copyright 2013-2014 Stephen Gold <sgold@sonic.net>
// Distributed under the terms of the GNU General Public License

/*
 This file is part of the Bats Game.

 The Bats Game is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by the
 Free Software Foundation, either version 3 of the License, or (at your
 option) any later version.

 The Bats Game is distributed in the hope that it will be useful, but
 WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 for more details.

 You should have received a copy of the GNU General Public License
 along with the Bats Game.  If not, see <http://www.gnu.org/licenses/>.
 */
package bats.ui;

import bats.Assets;
import bats.scene.SceneNode;
import com.jme3.app.Application;
import com.jme3.app.state.AbstractAppState;
import com.jme3.app.state.AppStateManager;
import com.jme3.cursors.plugins.JmeCursor;
import java.util.logging.Logger;
import jme3utilities.ui.InputMode;

/**
 * An app state which manages a scene, an input mode, and a heads-up display.
 * <p>
 * Multiple game modes may jointly manage the same scene.
 *
 * @author Stephen Gold <sgold@sonic.net>
 */
public class GameMode
        extends AbstractAppState {
    // *************************************************************************
    // constants

    /**
     * message logger for this class
     */
    final private static Logger logger =
            Logger.getLogger(GameMode.class.getName());
    // *************************************************************************
    // fields
    /**
     * corresponding input mode: set by constructor
     */
    final private GameInputMode inputMode;
    /**
     * true if the heads-up display is visible in this mode, false if the HUD
     * has been hidden: initialized by constructor
     */
    private boolean hudIsVisible;
    /**
     * true if physics can run in this mode, false if physics cannot run
     */
    private boolean willHavePhysics = false;
    /**
     * true if simulation is running in this mode, false if simulation is
     * paused; definitive only when the mode is disabled
     */
    private boolean willRunSimulation = false;
    /**
     * true if initialize() should enable this mode
     */
    private boolean startEnabled = false;
    /**
     * keep track of the currently enabled mode (null means there's none)
     */
    private static GameMode enabledMode = null;
    /**
     * corresponding heads-up display: set by constructor
     */
    final private HeadsUpDisplay hud;
    /**
     * scene which this mode controls: set by constructor
     */
    final private SceneNode scene;
    // *************************************************************************
    // constructors

    /**
     * Instantiate a disabled, uninitialized mode.
     *
     * @param showHud true if the heads-up display should be visible
     * @param scene which scene this mode controls (not null)
     * @param shortName terse name for the mode (not null)
     */
    public GameMode(boolean showHud, SceneNode scene, String shortName) {
        assert scene != null;
        assert shortName != null;

        hudIsVisible = showHud;
        this.scene = scene;
        /*
         * Determine the long name for this mode.
         */
        String longName = "";
        switch (shortName) {
            case "build":
                longName = "world-building";
                break;
            case "pose":
                longName = "pose";
                break;
            case "sim":
                longName = "simulation";
                break;
            default:
                assert false : shortName;
        }
        /*
         * Instantiate the input mode.
         */
        inputMode = new GameInputMode(this, shortName, longName);

        JmeCursor cursor = null;
        switch (shortName) {
            case "build":
                cursor = Assets.loadCursor("crosshairs/blue32.cur", 16, 16);
                inputMode.setSaveFileName("Bats build bindings.xml");
                break;
            case "pose":
                cursor = Assets.loadCursor("crosshairs/red32.cur", 16, 16);
                inputMode.setSaveFileName("Bats pose bindings.xml");
                break;
            case "sim":
                cursor = Assets.loadCursor("crosshairs/white32.cur", 16, 16);
                inputMode.setSaveFileName("Bats simulation bindings.xml");
                break;
            default:
                assert false : shortName;
        }
        inputMode.setCursor(cursor);
        /*
         * Instantiate the HUD.
         */
        if (shortName.equals("pose")) {
            hud = new PoseHud(inputMode, scene, shortName);
        } else {
            hud = new HeadsUpDisplay(inputMode, scene, shortName);
        }

        super.setEnabled(false);
        assert !isInitialized();
    }
    // *************************************************************************
    // new methods exposed

    /**
     * Find an initialized mode by its short name.
     *
     * @param shortName (not null)
     * @return the pre-existing instance (or null if none)
     */
    public static GameMode findMode(String shortName) {
        if (shortName == null) {
            throw new NullPointerException("name cannot be null");
        }
        InputMode mode = InputMode.findMode(shortName);
        if (mode instanceof GameInputMode) {
            GameInputMode bim = (GameInputMode) mode;
            return bim.getGameMode();
        }
        return null;
    }

    /**
     * Access the enabled mode, if any.
     *
     * @return pre-existing instance (or null if there's no enabled mode)
     */
    public static GameMode getEnabledMode() {
        return enabledMode;
    }

    /**
     * Access the heads-up display for this mode.
     *
     * @return pre-existing instance
     */
    public HeadsUpDisplay getHud() {
        assert hud != null;
        return hud;
    }

    /**
     * Read the short name of this mode.
     */
    public String getShortName() {
        String result = inputMode.getShortName();
        return result;
    }

    /**
     * Process an action.
     *
     * @param words the action string split into an array of words (not null)
     * @return true if the action was handled, false if it wasn't handled
     */
    public boolean processAction(String[] words) {
        assert words != null;

        boolean wasHandled = scene.getApplication().processAction(words);
        if (wasHandled) {
            return true;
        }
        wasHandled = scene.processAction(words);
        if (wasHandled) {
            return true;
        }
        wasHandled = hud.processAction(words);
        return wasHandled;
    }

    /**
     * Initialize the pause states of this mode.
     *
     * @param simulationPaused true if the mode starts with simulation and
     * animations paused, false if the mode starts with simulation running
     * @param physicsDisabled true if physics cannot run in this mode, false if
     * physics can run
     */
    public void setPauseStates(boolean simulationPaused,
            boolean physicsDisabled) {
        assert !initialized;

        willRunSimulation = !simulationPaused;
        willHavePhysics = !physicsDisabled;
    }
    // *************************************************************************
    // AbstractAppState methods

    /**
     * Initialize this (disabled) mode when it gets attached.
     *
     * @param stateManager (not null)
     * @param application (not null)
     */
    @Override
    public void initialize(AppStateManager stateManager,
            Application application) {
        assert !isInitialized();
        assert !isEnabled();

        super.initialize(stateManager, application);
        hud.initialize(stateManager, application);

        assert isInitialized();
        setEnabled(startEnabled);
    }

    /**
     * Enable or disable this mode.
     *
     * @param newState true to enable, false to disable
     */
    @Override
    public void setEnabled(boolean newState) {
        if (!isInitialized()) {
            startEnabled = newState;
            return;
        }

        boolean oldState = isEnabled();
        super.setEnabled(newState);
        inputMode.setEnabled(newState);

        if (!oldState && newState) {
            if (enabledMode != null) {
                String message = String.format(
                        "tried to enable %s game mode with %s mode active",
                        getShortName(), enabledMode.getShortName());
                throw new IllegalStateException(message);
            }
            enabledMode = this;

            hud.setEnabled(hudIsVisible);
            scene.attach();
            scene.setEnabled(willRunSimulation);
            if (willHavePhysics) {
                float speed = scene.getResumeSpeed();
                scene.getWorld().getPhysics().setSpeed(speed);
            }

        } else if (oldState && !newState) {
            assert enabledMode == this : enabledMode;
            enabledMode = null;

            hudIsVisible = hud.isEnabled();
            hud.setEnabled(false);

            willRunSimulation = scene.isEnabled();
            scene.setEnabled(false);
            scene.getWorld().getPhysics().setSpeed(0f);

            scene.detach();
        }
    }

    /**
     * Callback invoked when this mode gets attached.
     *
     * @param stateManager (not null)
     */
    @Override
    public void stateAttached(AppStateManager stateManager) {
        super.stateAttached(stateManager);

        stateManager.attach(inputMode);
    }

    /**
     * Update this mode. (Invoked once per frame.)
     *
     * @param simInterval elapsed time since the previous update (in seconds,
     * &ge;0)
     */
    @Override
    public void update(float simInterval) {
        assert simInterval >= 0f : simInterval;
        assert isEnabled();
        super.update(simInterval);

        hud.update(simInterval);
    }
}